#include "mesh_loader.h"
#include "GL/glew.h"
#include <iostream>


namespace kanon
{
    Mesh::Mesh()
    {
        vao = 0;
        vbo = 0;
        vbo_normal = 0;
        vbo_texture = 0;
        ibo = 0;
    }

    Mesh::~Mesh()
    {
        glDeleteBuffers(1, &vbo);
        glDeleteBuffers(1, &ibo);
        glDeleteVertexArrays(1, &vao);
    }

    void Mesh::Bind() const
    {
        glBindVertexArray(vao);
    }

    bool Model::fromGltf(const std::string& filepath)
    {
        tinygltf::Model model;
        tinygltf::TinyGLTF loader;
        std::string err, warn;
        bool ret = false;

        if (filepath.find(".glb") != std::string::npos)
            ret = loader.LoadBinaryFromFile(&model, &err, &warn, filepath.c_str());
        else
            ret = loader.LoadASCIIFromFile(&model, &err, &warn, filepath.c_str());

        if (!warn.empty())
            std::cout << "WARN: " << warn << std::endl;

        if (!err.empty()) {
            std::cerr << "ERR: " << err << std::endl;
        }
        if (!ret) {
            std::cerr << "Failed to parse glTF model: " << filepath << std::endl;
            return false;
        }

        meshes.clear();
        size_t num_meshes = model.meshes.size();
        for (size_t index = 0; index < num_meshes; ++index)
        {
            std::shared_ptr<Mesh> mesh = std::make_shared<Mesh>();
            bool ret = mesh->setupBuffers(model, index);
            if (!ret) continue;
            meshes.push_back(mesh);
        }

        return true;;
    }

    // Function to setup OpenGL buffers (VAO/VBO/EBO)
    bool Mesh::setupBuffers(const tinygltf::Model& model, int meshIndex)
    {
        const tinygltf::Mesh& mesh = model.meshes[meshIndex];
        if (mesh.primitives.empty()) 
            return false;

        // Assume one primitive per mesh for simplicity
        const tinygltf::Primitive& primitive = mesh.primitives[0];

        // Get accessor indices
        int posAccessor = primitive.attributes.at("POSITION");
        int normalAccessor = primitive.attributes.count("NORMAL") ? primitive.attributes.at("NORMAL") : -1;
        int texCoordAccessor = primitive.attributes.count("TEXCOORD_0") ? primitive.attributes.at("TEXCOORD_0") : -1;
        int indexAccessor = primitive.indices;

        // Extract data from accessors and buffer views
        const tinygltf::Accessor& posAcc = model.accessors[posAccessor];
        const tinygltf::BufferView& posView = model.bufferViews[posAcc.bufferView];
        const tinygltf::Buffer& posBuffer = model.buffers[posView.buffer];

        const tinygltf::Accessor& indexAcc = model.accessors[indexAccessor];
        const tinygltf::BufferView& indexView = model.bufferViews[indexAcc.bufferView];
        const tinygltf::Buffer& indexBuffer = model.buffers[indexView.buffer];

        // Create and bind VAO
        glGenVertexArrays(1, &vao);
        glBindVertexArray(vao);

        // Create and bind VBO for positions
        glGenBuffers(1, &vbo);
        glBindBuffer(GL_ARRAY_BUFFER, vbo);
        glBufferData(GL_ARRAY_BUFFER, posAcc.count * sizeof(float) * 3,
            &posBuffer.data[posAcc.byteOffset], GL_STATIC_DRAW);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
        glEnableVertexAttribArray(0);

        // Create and bind VBO for normals (if exists)
        if (normalAccessor != -1)
        {
            const tinygltf::Accessor& normalAcc = model.accessors[normalAccessor];
            const tinygltf::BufferView& normalView = model.bufferViews[normalAcc.bufferView];
            const tinygltf::Buffer& normalBuffer = model.buffers[normalView.buffer];

            glGenBuffers(1, &vbo_normal); // Reuse VBO or create a new one
            glBindBuffer(GL_ARRAY_BUFFER, vbo_normal);
            glBufferData(GL_ARRAY_BUFFER, normalAcc.count * sizeof(float) * 3,
                &normalBuffer.data[normalAcc.byteOffset], GL_STATIC_DRAW);
            glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
            glEnableVertexAttribArray(1);
        }

        // Create and bind VBO for texture coordinates (if exists)
        if (texCoordAccessor != -1)
        {
            const tinygltf::Accessor& texCoordAcc = model.accessors[texCoordAccessor];
            const tinygltf::BufferView& texCoordView = model.bufferViews[texCoordAcc.bufferView];
            const tinygltf::Buffer& texCoordBuffer = model.buffers[texCoordView.buffer];

            glGenBuffers(1, &vbo_texture); // Reuse VBO or create a new one
            glBindBuffer(GL_ARRAY_BUFFER, vbo_texture);
            glBufferData(GL_ARRAY_BUFFER, texCoordAcc.count * sizeof(float) * 2,
                &texCoordBuffer.data[texCoordAcc.byteOffset], GL_STATIC_DRAW);
            glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
            glEnableVertexAttribArray(2);
        }

        // Create and bind EBO for indices
        glGenBuffers(1, &ibo);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, indexAcc.count * sizeof(unsigned short),
            &indexBuffer.data[indexAcc.byteOffset], GL_STATIC_DRAW);

        num_indices = indexAcc.count;

        // Unbind VAO
        glBindVertexArray(0);
        return true;
    }

    void Mesh::Draw() const
    {
        glDrawElements(GL_TRIANGLES, num_indices, GL_UNSIGNED_SHORT, 0);
    }

#if 0
    void loadGLTFModel(const std::string& filename) {
        tinygltf::Model model;
        tinygltf::TinyGLTF loader;
        std::string err, warn;

        if (!loader.LoadASCIIFromFile(&model, &err, &warn, filename)) {
            std::cerr << "Failed to load glTF: " << err << std::endl;
            return;
        }

        for (const auto& mesh : model.meshes) 
        {
            Mesh m;
            glGenVertexArrays(1, &m.VAO);
            glBindVertexArray(m.VAO);

            for (const auto& primitive : mesh.primitives) 
            {
                const auto& indicesAccessor = model.accessors[primitive.indices];
                const auto& bufferView = model.bufferViews[indicesAccessor.bufferView];
                const auto& buffer = model.buffers[bufferView.buffer];
                const unsigned char* data = &buffer.data[bufferView.byteOffset + indicesAccessor.byteOffset];

                GLuint EBO;
                glGenBuffers(1, &EBO);
                glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
                glBufferData(GL_ELEMENT_ARRAY_BUFFER, bufferView.byteLength, data, GL_STATIC_DRAW);
                m.buffers.push_back(EBO);

                for (const auto& attr : primitive.attributes) {
                    const auto& accessor = model.accessors[attr.second];
                    const auto& view = model.bufferViews[accessor.bufferView];
                    const auto& buf = model.buffers[view.buffer];
                    const unsigned char* ptr = &buf.data[view.byteOffset + accessor.byteOffset];

                    GLuint buffer;
                    glGenBuffers(1, &buffer);
                    glBindBuffer(GL_ARRAY_BUFFER, buffer);
                    glBufferData(GL_ARRAY_BUFFER, view.byteLength, ptr, GL_STATIC_DRAW);
                    m.buffers.push_back(buffer);

                    int location = -1;
                    if (attr.first == "POSITION") location = 0;
                    else if (attr.first == "NORMAL") location = 1;
                    else if (attr.first == "TEXCOORD_0") location = 2;

                    if (location != -1) {
                        glEnableVertexAttribArray(location);
                        glVertexAttribPointer(location, accessor.type == TINYGLTF_TYPE_VEC3 ? 3 : 2,
                            accessor.componentType == TINYGLTF_COMPONENT_TYPE_FLOAT ? GL_FLOAT : GL_UNSIGNED_SHORT,
                            GL_FALSE, view.byteStride, (void*)(accessor.byteOffset));
                    }
                }
            }
            meshes[&mesh - &model.meshes[0]] = m;
        }
    }
#endif
}